torch.no_grad() is more aggressive, as it Autograd won’t track any operations in the block, so that out2 won’t have a valid .grad_fn and you thus won’t be able to call backward on the calculated loss.
.requires_grad will disable the grad calculation for this particular parameter, while the output of the operation will still have a .grad_fn, if one input had previously a .grad_fn (out1 in your example).
Here is an example for all three workflows:
class ExampleModel(torch.nn.Module):
    
    def __init__(self, dim) -> None:
        super(ExampleModel, self).__init__()
        self.linear = torch.nn.Linear(dim, dim)
    
    def forward(self, x, mode):
        out1 = self.linear(x)  
        if mode == 'original':
            out2 = self.linear(out1)
        elif mode == 'detach':
            out2 = F.linear(out1, self.linear.weight.detach(), self.linear.bias.detach())
        elif mode == 'req_grad':
            self.linear.requires_grad_(False)
            out2 = self.linear(out1)
            self.linear.requires_grad_(True)
        else:
            raise RuntimeError
        return out2
    
torch.manual_seed(2809)
# Random input output data for this example
N, D = 64, 100
x = torch.randn(N, D)
y = torch.randn(N, D)
    
model = ExampleModel(D)
criterion = torch.nn.MSELoss(reduction='sum')
    
# Original
y_pred = model(x, mode='original')
loss = criterion(y_pred, y)
loss.backward()

weight_grad_ref = model.linear.weight.grad.clone()
bias_grad_ref = model.linear.bias.grad.clone()
model.zero_grad()

# Detach
y_pred = model(x, mode='detach')
loss = criterion(y_pred, y)
loss.backward()

weight_grad_detach = model.linear.weight.grad.clone()
bias_grad_detach = model.linear.bias.grad.clone()
model.zero_grad()

#Req_grad
y_pred = model(x, mode='req_grad')
loss = criterion(y_pred, y)
loss.backward()

weight_grad_req_grad = model.linear.weight.grad.clone()
bias_grad_req_grad = model.linear.bias.grad.clone()
model.zero_grad()

# Compare
print((weight_grad_ref - weight_grad_detach).abs().max())
print((weight_grad_detach - weight_grad_req_grad).abs().max())

As you can see, the “detach” and “requires_grad” approach yield the same output.