[Bug] `sklearn.base.BaseEstimator` subclasses not decoratable

### Describe the bug

Greetings, wondrous `sklearn` maintainers. This is @leycec, the maintainer of @beartype – a third-generation hybrid static-runtime type-checker with far too many hyphens in its description. Who even knows what that means at this point. The point is that I recently [fielded an issue](https://github.com/beartype/beartype/issues/340) from [one of my favourite users](https://github.com/tvdboom) requesting support for `sklearn` "metadata routing." I don't even know what that is... *but it sounds hot.*

Sadly, @beartype cannot provide this support – because the `sklearn.base.BaseEstimator` subclass fails to support decoration at a core level. That's outside our scope of control. But first, the minimal reproducible example (MRE) exhibiting this issue:

### Steps/Code to Reproduce

```python
import sklearn
from sklearn.base import BaseEstimator

sklearn.set_config(enable_metadata_routing=True)

class A(BaseEstimator):
    def fit(self, X, y, sample_weight=None):
        return self

A.set_fit_request = A.set_fit_request
a = A().set_fit_request(sample_weight=True)
```

### Expected Results

No exception is raised.

### Actual Results

```python
Traceback (most recent call last):
  File "/home/leycec/tmp/mopy.py", line 13, in <module>
    a = A().set_fit_request(sample_weight=True)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: RequestMethod.__get__.<locals>.func() takes 0 positional arguments but 1 was given
```

### Versions

```shell
System:
    python: 3.12.2 (main, Mar  9 2024, 18:58:45) [GCC 13.2.1 20240113]
executable: /usr/bin/python3.12
   machine: Linux-6.1.67-gentoo-x86_64-AMD_Athlon-tm-_II_X2_240_Processor-with-glibc2.38

Python dependencies:
      sklearn: 1.3.2
          pip: 24.0
   setuptools: 69.1.1
        numpy: 1.26.4
        scipy: 1.12.0
       Cython: 3.0.8
       pandas: 2.2.0
   matplotlib: 3.8.3
       joblib: 1.3.2
threadpoolctl: 3.3.0

Built with OpenMP: True

threadpoolctl info:
       user_api: blas
   internal_api: openblas
    num_threads: 6
         prefix: libopenblas
       filepath: /usr/lib64/libopenblas_barcelonap-r0.3.25.so
        version: 0.3.25
threading_layer: openmp
   architecture: BARCELONA

       user_api: openmp
   internal_api: openmp
    num_threads: 2
         prefix: libgomp
       filepath: /usr/lib/gcc/x86_64-pc-linux-gnu/13/libgomp.so.1.0.0
        version: None
```
```

COMMENT:
**Further details follow.** In the above example, commenting out the assignment `A.set_fit_request = A.set_fit_request` prevents the exception from being raised. But that assignment is a **tautology.** Since that assignment reduces to a noop, that assignment should have *no* harmful side effects. But it does. You are probably now thinking to yourself:

> "That has nothing to do with decoration. You lied, @leycec! *You lied about everything.*"

*...heh.* Look. Type-checking is non-trivial. Surprisingly, the above example has *everything* to do with type-checking. Under the hood, type-checkers like @beartype and `typeguard` support class decoration via the `setattr()` builtin. The pseudocode for this sorta resembles:

```python
def beartype(cls: type) -> type:
    # For the unqualified name and value of each direct (i.e., *NOT* indirectly
    # inherited) attribute of this class...
    for attr_name, attr_value in cls.__dict__.items():
        # Replace this undecorated attribute with this decorated attribute.
        setattr(cls, attr_name, beartype(attr_value))
```

I trust we all now see the relevancy. Internally, @beartype decorates classes by dynamically replacing undecorated class attributes with decorated class attributes. This is similar to the assignment `A.set_fit_request = A.set_fit_request` above if you squint hard at it.

Because `BaseEstimator` fails to support attribute assignment, `BaseEstimator` *also* fails to support decoration by runtime type-checkers. Ergo, the following example raises the same exception as above:

```python
import sklearn
from sklearn.base import BaseEstimator
from beartype import beartype

sklearn.set_config(enable_metadata_routing=True)

@beartype
class A(BaseEstimator):
    def fit(self, X, y, sample_weight=None):
        return self

a = A().set_fit_request(sample_weight=True)  # <-- *BOOM!* same exception, yo
```

@beartype users can manually circumvent this by decorating `BaseEstimator` *methods* rather than *subclasses*. This approach raises no exception:

```python
import sklearn
from sklearn.base import BaseEstimator
from beartype import beartype

sklearn.set_config(enable_metadata_routing=True)

class A(BaseEstimator):
    @beartype  # <-- shift the decoration from the class to method
    def fit(self, X, y, sample_weight=None):
        return self

a = A().set_fit_request(sample_weight=True)  # <-- woah. this actually works.
```

**That works,** because the `BaseEstimator` superclass is no longer involved. Users shouldn't have to go through extreme hoops, though. `BaseEstimator` should behave sanely out-of-the-box. Thus spake @beartype. :bear:

Labels: Bug